package com.cui.project.recommend.service.impl;

import com.api.common.alert.CommonAlertHelper;
import com.api.common.model.entity.InterfaceInfo;
import com.api.common.model.entity.InterfaceInfoLog;
import com.api.common.model.entity.UserInterfaceFavorite;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cui.project.mapper.InterfaceInfoMapper;
import com.cui.project.mapper.UserInterfaceFavoriteMapper;
import com.cui.project.mapper.InterfaceInfoLogMapper;
import com.cui.project.recommend.model.dto.RecommendItem;
import com.cui.project.recommend.service.RecommendService;
import com.cui.project.recommend.util.TfidfUtil;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StopWatch;


import javax.annotation.Resource;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;


/**
 * 推荐核心（调用日志 + 收藏 + 文本相似）
 *
 * 运行逻辑总览：
 * 1）在内存中维护三类“模型数据”：
 *    a. itemNeighbors：ItemCF 得到的“接口相似邻居表”
 *    b. itemPopularity：7天 EMA 热度
 *    c. tfidf：TF-IDF 内容相似向量
 * 2）rebuildModels()（定时/手动）：
 *    - 读取近30天成功调用日志 + 有效收藏，构造“用户-接口偏好矩阵”
 *    - 基于偏好计算 ItemCF 共现相似度，生成邻居表（Top50）
 *    - 加载全部接口文本，训练 TF-IDF（用于内容相似）
 *    - 计算近7天调用+收藏的 EMA 热度
 * 3）recommendForUser(userId, topK)：
 *    - 汇总该用户最近60天的成功调用 + 收藏，形成“用户偏好”
 *    - 用偏好里的接口作为“种子”，通过：
 *      a. CF：查邻居表做协同过滤召回
 *      b. Content：TF-IDF 内容相似召回
 *      c. Popularity：热度兜底
 *    - 将三路得分按权重融合，并适度对“有收藏史的用户”整体抬权
 *    - 去除用户已使用/收藏过的接口，排序截断返回
 * 4）favorite(userId, interfaceId, enable)：
 *    - 幂等收藏/取消收藏
 *
 * 分析用了那些算法：
 *      ItemCF（基于物品的协同过滤）
 *      构造用户-接口偏好：成功调用计数权重 W_CALL，收藏加权 W_FAV（> 调用）。
 *      共现矩阵：遍历同一用户使用过的接口对 (a,b)，累计共现强度。
 *      对数缩放：对用户的 item 偏好作 log(2 + preference) 缩放，减弱重度用户的偏置。
 *      相似度归一化：用“伪向量余弦”sim(a,b) = co(a,b)/sqrt(norm(a)·norm(b)) 得到 邻居表（Top-K 截断）。
 *
 *      内容相似召回（Content-based）
 *      基于 TF-IDF + 余弦，用用户偏好 TopN 接口作为种子，取其最相近的接口聚合得分。
 *
 *      热度/流行度（Popularity）
 *      由近 7 天日志构造 EMA（指数滑动平均） 作为热度：
 *      EMA_t = α·x_t + (1-α)·EMA_{t-1}（α≈0.3）。调用和收藏各计 1。
 *
 *      多源融合打分（Hybrid Ranking）
 *      加权求和：score = W_CF*CF + W_CONTENT*Content + W_POP*Popularity；
 *      若用户有收藏史，再给整体轻微加权（FAV_BONUS）。
 *      无历史时 冷启动兜底 用热度榜。
 *
 *      去重与截断
 *      过滤用户已用/已收藏项，按分数排序取 Top-K。
 */
@Slf4j
@Service
public class RecommendServiceImpl implements RecommendService {

    @Resource
    private  InterfaceInfoLogMapper logMapper;
    @Resource
    private  InterfaceInfoMapper infoMapper;
    @Resource
    private  UserInterfaceFavoriteMapper favMapper;
    @Resource
    private CommonAlertHelper alertHelper;

    // ===== 内存“模型”存储（线程安全，读多写少） =====
    private final Cache<Long, Map<Long, Double>> itemNeighbors = CacheBuilder.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build();;// 每个接口 -> 相似接口及相似度
    private final Map<Long, Double> itemPopularity = new ConcurrentHashMap<Long, Double>();  // 接口热度（EMA）
    private final TfidfUtil tfidf = new TfidfUtil();// TF-IDF 工具（内部含向量）
    private volatile Set<Long> allInterfaceIds = new HashSet<Long>();// 接口全集 ID 集合（volatile 保证可见性）

    // ===== 权重配置（可按效果调优） =====
    @Value("${recommend.weight.cf:0.55}")
    private  double W_CF = 0.55;       // 协同过滤结果权重
    @Value("${recommend.weight.content:0.30}")
    private double W_CONTENT = 0.30;  // 内容相似结果权重
    @Value("${recommend.weight.pop:0.10}")
    private  double W_POP = 0.10;      // 热度兜底权重
    @Value("${recommend.weight.fav_bonus:0.05}")
    private  double W_FAV_BONUS = 0.05;// 有收藏史用户整体抬权比
    @Value("${recommend.weight.call:1.0}")
    private  double W_CALL = 1.0;      // 成功调用一次的偏好权重
    @Value("${recommend.weight.fav:6.0}")
    private  double W_FAV  = 6.0;      // 收藏一次的偏好权重（> 调用

    // ===== 日志成功判定：基于 api.sql 中 interface_info_log.code 字段值 200 =====
    private boolean isSuccess(InterfaceInfoLog l) { // 判定一条日志是否为“成功调用”
        return l.getCode() != null && l.getCode() == 200; // code == 200 认定为成功
    }

    /**
     * 对指定用户返回 TopK 推荐
     *
     * 个性化推荐主流程
     * 运行逻辑：
     * 1）取用户近60天成功调用日志 + 有效收藏，形成“用户偏好”Map<iid, score>
     * 2）协同过滤召回：遍历用户偏好里的每个“种子接口”，累加其邻居分数（按用户对该种子的偏好加权）
     * 3）内容相似召回：取偏好Top5作为内容种子，使用 TF-IDF 相似接口累加
     * 4）热度兜底：取近7天 EMA 热度
     * 5）融合打分：W_CF*CF + W_CONTENT*Content + W_POP*Popularity (+ 收藏整体小幅加权)
     * 6）去重（过滤已使用/收藏），排序截断 TopK
     * @param userId
     * @param topK
     * @return
     */
    @Override
    public List<RecommendItem> recommendForUser(Long userId, int topK) {
        Instant since60d = Instant.now().minus(60, ChronoUnit.DAYS);// 计算 60 天前的时间点

        // —— 用户调用偏好（近60天成功调用）——
        List<InterfaceInfoLog> logs = logMapper.selectList(
                new LambdaQueryWrapper<InterfaceInfoLog>()
                        .eq(InterfaceInfoLog::getUser_id, userId)
                        .ge(InterfaceInfoLog::getCreateTime, Date.from(since60d))
        );

        // 用户偏好：接口ID -> 分值
        Map<Long, Double> pref = new HashMap<Long, Double>();
        for (InterfaceInfoLog l : logs){

            if (!isSuccess(l)) continue;
            Long iid = l.getInterface_info_id();
            Double old = pref.get(iid);
            // 成功调用一次累加 W_CALL
            pref.put(iid, (old == null ? 0.0 : old) + W_CALL);
        }

        // —— 用户收藏偏好（长期有效收藏，不限时间）——
        List<UserInterfaceFavorite> favs = favMapper.selectList(
                new LambdaQueryWrapper<UserInterfaceFavorite>()
                        .eq(UserInterfaceFavorite::getUserId, userId)
                        .eq(UserInterfaceFavorite::getStatus, 1)
        );
        for (UserInterfaceFavorite f : favs) { // 遍历收藏
            Long iid = f.getInterfaceInfoId();
            Double old = pref.get(iid);
            // 收藏一次累加 W_FAV（权重大于调用）
            pref.put(iid, (old == null ? 0.0 : old) + W_FAV);
        }
        // 用户已经“使用/收藏”的接口集合，用于后续去重
        Set<Long> used = new HashSet<Long>(pref.keySet());

        // —— 协同过滤召回（CF）——
        Map<Long, Double> cf = new HashMap<Long, Double>();// CF 得分表：候选接口ID -> 分数
        for (Long item : pref.keySet()) { // 遍历用户偏好中的每一个“种子接口”
            Map<Long, Double> neigh = itemNeighbors.getIfPresent(item); // 取该接口的相似邻居表
            if (neigh == null) continue; // 无邻居则跳过
            double w = Math.log(2 + pref.get(item)); // 用户对该种子接口的权重（log缩放抑制大户影响）
            for (Map.Entry<Long, Double> e : neigh.entrySet()) { // 遍历邻居
                if (used.contains(e.getKey())) continue; // 已使用/收藏的过滤掉 （如果把用户已经调用过的接口（或者已收藏的接口）再次作为候选推荐，就失去了推荐的意义，结果看起来会像“把用户自己用过的东西又推回去”，体验很差。）
                Double old = cf.get(e.getKey()); // 取旧分
                cf.put(e.getKey(), (old == null ? 0.0 : old) + w * e.getValue()); // 累加分数：权重 * 相似度
            }
        }
        // —— 内容相似召回（Content-based, TF-IDF 余弦）（取偏好 Top5 作为种子）——
        Map<Long, Double> content = new HashMap<Long, Double>(); // 内容相似得分表
        // 将用户偏好按分值降序，取前5个作为“内容种子”
        List<Map.Entry<Long, Double>> prefSorted = new ArrayList<Map.Entry<Long, Double>>(pref.entrySet()); // 偏好条目列表
        Collections.sort(prefSorted, new Comparator<Map.Entry<Long, Double>>() { // 降序排序
            @Override public int compare(Map.Entry<Long, Double> o1, Map.Entry<Long, Double> o2) {
                return Double.compare(o2.getValue(), o1.getValue());
            }
        });
        int seedCnt = Math.min(5, prefSorted.size()); // 种子个数至多5个 （偏好分最高的前 5 个接口）
        for (int i=0;i<seedCnt;i++){ // 遍历每个种子
            Long seed = prefSorted.get(i).getKey(); // 种子接口ID
            Map<Long, Double> simMap = tfidf.topKSimilar(seed, 50); // 取与该种子最相似的50个接口
            for (Map.Entry<Long, Double> e : simMap.entrySet()){ // 遍历相似结果
                if (used.contains(e.getKey())) continue; // 已使用/收藏过滤
                Double old = content.get(e.getKey()); // 旧分
                content.put(e.getKey(), (old == null ? 0.0 : old) + e.getValue()); // 累加相似度分数
            }
        }

        /**
         * —— 热度兜底（Popularity by EMA）——
         * 作用：
         *  用户 1001（老用户）
         *  有大量调用 + 收藏 → CF + 内容相似召回够丰富
         *  热度兜底只是在候选不够时补几个热门接口
         *
         *  用户 1003（轻度用户）
         *  偏好很稀疏，只用过天气 + 空气质量
         *  CF/内容相似召回有限，热度兜底会提供更多候选（比如“新闻热榜”、“逆地理编码”）
         *
         *  新用户 1004（完全没有行为）
         *  CF/内容相似全空 → 最终结果就是纯热度榜（热门推荐）
         *
         *  热度兜底保证了推荐系统在“用户冷启动 / 数据稀疏 / 新接口推广”场景下依然有东西可推荐，不会出现空列表。
         */
        Map<Long, Double> pop = itemPopularity; // 直接使用内存热度表
        boolean hasFav = !favs.isEmpty(); // 该用户是否有收藏史（用于整体微抬）

        // —— 融合打分：W_CF*CF + W_CONTENT*Content + W_POP*Popularity (+ 收藏整体小幅加权) ——
        Map<Long, Double> total = new HashMap<Long, Double>(); // 融合总分
        for (Long iid : allInterfaceIds) { // 遍历所有接口ID
            if (used.contains(iid)) continue; // 已使用/收藏过滤
            double s = W_CF * (cf.get(iid) == null ? 0.0 : cf.get(iid)) // CF分
                     + W_CONTENT * (content.get(iid) == null ? 0.0 : content.get(iid)) // 内容分
                     + W_POP * (pop.get(iid) == null ? 0.0 : pop.get(iid)); // 热度分
            if (s > 0 && hasFav) s += W_FAV_BONUS * s; // 有收藏史的用户整体加成（轻微）
            if (s > 0) total.put(iid, s); // 正分才纳入候选
        }
        if (total.isEmpty()){
//            total.putAll(pop); // 若无任何候选（极冷启动），用热度榜兜底
            // 冷启动：返回热度Top-K
            List<Map.Entry<Long, Double>> hotList = new ArrayList<>(pop.entrySet());
            Collections.sort(hotList, (a, b) -> Double.compare(b.getValue(), a.getValue()));

            for (int i = 0; i < Math.min(topK, hotList.size()); i++) {
                total.put(hotList.get(i).getKey(), hotList.get(i).getValue());
            }
        }

        // —— 排序 + 截断 TopK ——
        List<Map.Entry<Long, Double>> list = new ArrayList<Map.Entry<Long, Double>>(total.entrySet()); // 候选列表
        Collections.sort(list, new Comparator<Map.Entry<Long, Double>>() { // 按分数降序
            @Override public int compare(Map.Entry<Long, Double> o1, Map.Entry<Long, Double> o2) {
                return Double.compare(o2.getValue(), o1.getValue());
            }
        });

        List<RecommendItem> res = new ArrayList<RecommendItem>(); // 返回结果列表
        int i = 0; // 计数
        for (Map.Entry<Long, Double> e: list){ // 遍历排序后的候选
            if (i++ >= topK) break; // 超出TopK则停止
            String reason; // 推荐理由
            if (cf.containsKey(e.getKey())) reason = "与你常用/收藏接口相似"; // 有CF分 → 协同过滤
            else if (content.containsKey(e.getKey())) reason = "内容相似"; // 有内容分 → 内容召回
            else reason = "热门推荐"; // 否则 → 热度兜底

            RecommendItem r = new RecommendItem(); // 构造 DTO
            r.setInterfaceId(e.getKey()); // 设置接口ID
            r.setScore(e.getValue()); // 设置得分
            r.setReason(reason); // 设置理由
            r.setTag(reason.contains("热门") ? "hot" : (reason.contains("收藏") ? "fav" : "similar")); // 设置标签
            res.add(r); // 加入结果
        }
        return res; // 返回 TopK 推荐
    }
    /**
     * 收藏/取消收藏（幂等）
     * 运行逻辑：
     * 1）按 userId + interfaceId 查询是否已有记录
     * 2）无记录则插入一条（status=1/0）
     * 3）有记录则更新 status
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void favorite(Long userId, Long interfaceId, boolean enable) { // enable=true收藏；false取消
        UserInterfaceFavorite old = favMapper.selectOne(
                new LambdaQueryWrapper<UserInterfaceFavorite>()
                        .eq(UserInterfaceFavorite::getUserId, userId)
                        .eq(UserInterfaceFavorite::getInterfaceInfoId, interfaceId)
        );
        if (old == null){
            UserInterfaceFavorite f = new UserInterfaceFavorite();
            f.setUserId(userId);
            f.setInterfaceInfoId(interfaceId);
            f.setStatus(enable ? 1 : 0);
            favMapper.insert(f);
        } else {
            old.setStatus(enable ? 1 : 0);
            favMapper.updateById(old);
        }
    }

    /**
     * 模型重建
     *
     * 模型重建（定时/手动）
     * 运行逻辑：
     * 1）加载“接口全集 + 文本” → 训练 TF-IDF（内容相似用）
     * 2）加载“近30天成功调用 + 有效收藏”，构造用户-接口偏好
     * 3）基于偏好计算 ItemCF 共现相似度，得到“邻居Top50”
     * 4）计算“近7天 调用+收藏 的 EMA 热度”，作为热度兜底
     */
    @Override
    @Async("recommendExecutor") // 使用专用的推荐计算线程池（CPU密集型）
    @Scheduled(fixedDelay = 5 * 60 * 1000L, initialDelay = 20_000L) //每5分钟自动重建；启动后延迟20秒首次执行
    public void rebuildModels() {
        StopWatch stopWatch=new StopWatch();
        stopWatch.start();
        try { // 捕获异常避免任务线程崩溃
            Instant now = Instant.now(); // 当前时刻
            Instant t30d = now.minus(30, ChronoUnit.DAYS); // 30天前
            Instant t7d  = now.minus(7,  ChronoUnit.DAYS); // 7天前

            // —— 1) 接口全集 + 文本 → 训练 TF-IDF ——
            List<InterfaceInfo> infos = infoMapper.selectList(null); // 全量接口
            Set<Long> ids = new HashSet<Long>(); // 接口ID集合
            List<TfidfUtil.Doc> docs = new ArrayList<TfidfUtil.Doc>(); // TF-IDF 文档列表
            for (InterfaceInfo i : infos){ // 遍历接口
                ids.add(i.getId()); // 收集ID
                String text = (nvl(i.getName()) + " " + nvl(i.getDescription())).trim(); // 组装文本：名称+描述
                if(!text.isEmpty())//过滤空文本
                    docs.add(new TfidfUtil.Doc(i.getId(), text)); // 构造文档
            }
            allInterfaceIds = ids; // 保存接口ID全集（volatile，供推荐时遍历）
            tfidf.fit(docs); // 训练 TF-IDF（构建向量与索引）

            // —— 2) 近30天成功调用 ——
            List<InterfaceInfoLog> logs30raw = logMapper.selectList( // 查询近30天日志
                    new LambdaQueryWrapper<InterfaceInfoLog>()
                            .ge(InterfaceInfoLog::getCreateTime, Date.from(t30d))
            );
            List<InterfaceInfoLog> logs30 = new ArrayList<InterfaceInfoLog>(); // 查询近30天日志
            for (InterfaceInfoLog l : logs30raw) if (isSuccess(l)) logs30.add(l); // 过滤 code==200

            // —— 2.1) 有效收藏（全量取出，用于偏好与热度）——
            List<UserInterfaceFavorite> favs = favMapper.selectList(
                    new LambdaQueryWrapper<UserInterfaceFavorite>().eq(UserInterfaceFavorite::getStatus, 1)
            );

            // —— 3) 构造 用户-接口 偏好矩阵（调用+收藏）——
            Map<Long, Map<Long, Double>> user2item = new HashMap<Long, Map<Long, Double>>(); // userId -> (iid -> score)
            for (InterfaceInfoLog l : logs30){ // 遍历成功调用
                Map<Long, Double> row = user2item.get(l.getUser_id()); // 取该用户行
                if (row == null){ row = new HashMap<Long, Double>(); user2item.put(l.getUser_id(), row); } // 无则新建
                Long iid = l.getInterface_info_id(); // 接口ID
                Double old = row.get(iid); // 旧分
                row.put(iid, (old == null ? 0.0 : old) + W_CALL); // 累加调用权重
            }
            for (UserInterfaceFavorite f : favs){ // 遍历收藏
                Map<Long, Double> row = user2item.get(f.getUserId()); // 取该用户行
                if (row == null){ row = new HashMap<Long, Double>(); user2item.put(f.getUserId(), row); } // 无则新建
                Long iid = f.getInterfaceInfoId(); // 接口ID
                Double old = row.get(iid); // 旧分
                row.put(iid, (old == null ? 0.0 : old) + W_FAV); // 累加收藏权重
            }

            // —— 3.1) ItemCF 共现统计 + 相似度归一化（余弦形式）——
            Map<Long, Map<Long, Double>> co = new HashMap<Long, Map<Long, Double>>(); // 共现矩阵（未归一）
            Map<Long, Double> norm = new HashMap<Long, Double>(); // 每个item的“自范数平方”累加（用于归一）

            for (Map<Long, Double> prefRow : user2item.values()){ // 遍历每个用户的偏好行
                List<Long> items = new ArrayList<Long>(prefRow.keySet()); // 用户用过/收藏过的 item 列表
                for (int i=0;i<items.size();i++){ // 双重循环生成 item 两两组合
                    long a = items.get(i); // 物品a
                    double wa = Math.log(2 + prefRow.get(a)); // 对用户对物品a的偏好做 log 缩放
                    Double na = norm.get(a); // 取 a 的累加平方
                    norm.put(a, (na == null ? 0.0 : na) + wa*wa); // 自范数平方累加

                    for (int j=i+1;j<items.size();j++){ // 与后续的 b 配对
                        long b = items.get(j); // 物品b
                        double wb = Math.log(2 + prefRow.get(b)); // 对 b 的偏好也做 log 缩放
                        double add = wa * wb; // 共现强度增量（近似内积）

                        Map<Long, Double> rowA = co.get(a); // a 行
                        if (rowA == null){ rowA = new HashMap<Long, Double>(); co.put(a, rowA); } // 无则新建
                        Double oldAB = rowA.get(b); // a->b 旧值
                        rowA.put(b, (oldAB == null ? 0.0 : oldAB) + add); // 累加 a->b

                        Map<Long, Double> rowB = co.get(b); // b 行
                        if (rowB == null){ rowB = new HashMap<Long, Double>(); co.put(b, rowB); } // 无则新建
                        Double oldBA = rowB.get(a); // b->a 旧值
                        rowB.put(a, (oldBA == null ? 0.0 : oldBA) + add); // 累加 b->a（对称）
                    }
                }
            }

            Map<Long, Map<Long, Double>> neigh = new HashMap<Long, Map<Long, Double>>(); // 最终邻居表
            for (Map.Entry<Long, Map<Long, Double>> e : co.entrySet()){ // 遍历每个 item 的共现行
                long a = e.getKey(); // 物品a
                double na = Math.sqrt(getOrDefault(norm, a, 1.0)); // a 的范数
                Map<Long, Double> row = new HashMap<Long, Double>(); // a 的邻居相似度行
                for (Map.Entry<Long, Double> f : e.getValue().entrySet()){ // 遍历 a 的每个邻居 b
                    long b = f.getKey(); // 物品b
                    double nb = Math.sqrt(getOrDefault(norm, b, 1.0)); // b 的范数
                    row.put(b, f.getValue() / (na*nb + 1e-9)); // 归一化得到“余弦相似”
                }
                // 截断 Top50（避免噪声和节省内存）
                List<Map.Entry<Long, Double>> entries = new ArrayList<Map.Entry<Long, Double>>(row.entrySet());
                Collections.sort(entries, new Comparator<Map.Entry<Long, Double>>() { // 降序
                    @Override public int compare(Map.Entry<Long, Double> o1, Map.Entry<Long, Double> o2) {
                        return Double.compare(o2.getValue(), o1.getValue());
                    }
                });
                LinkedHashMap<Long, Double> top = new LinkedHashMap<Long, Double>(); // 有序TopK
                int cnt = 0; // 计数
                for (Map.Entry<Long, Double> kv : entries){ // 遍历排序后的邻居
                    if (cnt++ >= 50) break; // 超过50个则截断
                    top.put(kv.getKey(), kv.getValue()); // 放入Top表
                }
                neigh.put(a, top); // 写回 a 的邻居表
            }
            itemNeighbors.invalidateAll(); // 清空旧邻居表
            itemNeighbors.putAll(neigh); // 用新邻居表替换

            // —— 4) 热度（近7天 EMA；调用+收藏各记1）——
            List<InterfaceInfoLog> logs7raw = logMapper.selectList( // 查询近7天日志
                    new LambdaQueryWrapper<InterfaceInfoLog>()
                            .ge(InterfaceInfoLog::getCreateTime, Date.from(t7d))
            );
            List<InterfaceInfoLog> logs7 = new ArrayList<InterfaceInfoLog>(); // 成功调用集合
            for (InterfaceInfoLog l : logs7raw) if (isSuccess(l)) logs7.add(l); // 过滤成功
            Collections.sort(logs7, new Comparator<InterfaceInfoLog>() { // 按时间递增
                @Override public int compare(InterfaceInfoLog o1, InterfaceInfoLog o2) {
                    return o1.getCreateTime().compareTo(o2.getCreateTime());
                }
            });

            Map<Long, Double> ema = new HashMap<Long, Double>(); // EMA 热度表
            double alpha = 0.3; // EMA 平滑系数
            for (InterfaceInfoLog l : logs7){
                long iid = l.getInterface_info_id(); // 遍历成功调用
                Double prev = ema.get(iid); // 之前热度
                ema.put(iid, (prev == null ? 0.0 : prev)*(1-alpha) + alpha*1.0); // 调用计1次
            }
            // 近7天收藏也计入热度（若收藏较多可单独加权）
            List<UserInterfaceFavorite> fav7 = new ArrayList<UserInterfaceFavorite>(); // 近7天收藏
            for (UserInterfaceFavorite f : favs){
                if (f.getUpdateTime() == null || f.getUpdateTime().toInstant().isAfter(t7d)) { // 收录近7天内更新/新增的收藏
                    fav7.add(f);
                }
            }
            for (UserInterfaceFavorite f : fav7){ // 遍历收藏
                long iid = f.getInterfaceInfoId(); // 接口ID
                Double prev = ema.get(iid); // 之前热度
                ema.put(iid, (prev == null ? 0.0 : prev)*(1-alpha) + alpha*1.0); // 收藏计1次
            }
            itemPopularity.clear(); // 清空旧热度
            itemPopularity.putAll(ema); // 写入新热度
            // 打印模型规模
            log.info("[Reco] rebuilt: items={}, neighbors={}", Integer.valueOf(allInterfaceIds.size()), Long.valueOf(itemNeighbors.size()));

            stopWatch.stop();
            //如果重建时间过长（超过2分钟），发送告警
            long duration = stopWatch.getTotalTimeMillis();
            if(duration>120000){
                alertHelper.recommendModelSlow(
                        "ItemCF+TF-IDF",
                        duration,
                        allInterfaceIds!=null?allInterfaceIds.size():0
//                        "195883938@qq.com"  //  产品经理邮箱
                );
            }

        } catch (Exception e){ // 捕获异常
            log.warn("[Reco] rebuild failed", e); // 日志警告

            // 发送告警（邮件/钉钉/企微）
            alertHelper.recommendModelRebuildFailure("ItemCF+TF-IDF", e.getMessage());
        }
    }
    // 工具：null 转空串
    private static String nvl(String s){ return s==null? "" : s; } // 避免 TF-IDF 拼接文本时 NPE

    // 工具：Map getOrDefault 的 JDK8 手写版（double）
    private static double getOrDefault(Map<Long, Double> m, Long k, double def){ // 取值或默认
        Double v = m.get(k); // 读取值
        return v == null ? def : v.doubleValue(); // 返回值或默认
    }
}